[FEAT] Provider Budgets#45
Conversation
- Add background job to periodically refresh provider billing metrics every 15 minutes - Implement billing adapters for OpenAI (project costs/alerts) and OpenRouter (key limits/credits) - Add admin REST API for listing, getting, updating, and manually refreshing billing connections - Support API key encryption for billing credentials with mandatory `ENCRYPTION_KEY` checks - Integrate local spend tracking from `usage_events` to show comparative monthly consumption - Update frontend provider settings with billing status, progress bars, and configuration tabs - Bump `omniference` to 0.3.1
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThis change adds provider billing storage, upstream OpenAI/OpenRouter metric retrieval, scheduled refreshes, admin APIs, and settings-page controls. It also introduces persistent credential encryption with versioned ciphertext, legacy compatibility, typed errors, and propagated decryption failures. ChangesProvider billing and encryption
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
| .unwrap_or_else(|| match row.provider_kind { | ||
| ProviderKind::Google | ProviderKind::OpenaiCompat | ProviderKind::Custom => ProviderBillingStatus::Unsupported, |
There was a problem hiding this comment.
Anthropic Gets Configurable Status
Anthropic is rejected as unsupported by the update route, but this fallback returns NOT_CONFIGURED for it. Every Anthropic card therefore shows billing as setup-required even though the API will reject any attempt to enable it.
Context Used: AGENTS.md (source)
| function billingStatusKey(billing: ProviderBillingOverview) { | ||
| if ((billing.status === 'UPSTREAM_ERROR' || billing.status === 'UNAUTHORIZED') && billing.upstream) return 'settings.providers.billing.failed'; | ||
| if (billing.status === 'AVAILABLE' && billing.upstream) return 'settings.providers.billing.provider_reported'; | ||
| if (billing.status === 'UNSUPPORTED') return 'settings.providers.billing.unsupported'; | ||
| if (billing.status === 'NOT_CONFIGURED' || billing.status === 'UNAUTHORIZED') return 'settings.providers.billing.setup_required'; | ||
| return 'settings.providers.billing.failed'; | ||
| } |
There was a problem hiding this comment.
Unauthorized State Looks Unconfigured
When the first refresh receives 401 or 403, the overview has UNAUTHORIZED with no cached upstream metric. This mapping labels that state as setup-required rather than a failed update, so an admin who supplied an invalid key receives the wrong diagnosis.
Context Used: AGENTS.md (source)
…e encryption utilities - Remove unused `.plan/provider-billing-overview.md` file - Update encryption utilities: - Add `CredenatialDecryptionFailed` error and include it in upstream billing workflow - Refactor decryption logic and improve error handling for credentials - Extend `utils/encryption.rs` with a secure local key management feature for encryption initialization
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 14
🧹 Nitpick comments (3)
src/types/providers/mod.rs (1)
16-16: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDo not re-export the private SQL row type.
Keep
ProviderBillingOverviewRowinternal to the billing repository/row implementation instead of exposing it through the domain entry point.Proposed adjustment
-pub(crate) use billing::{ProviderBillingConnection, ProviderBillingMetric, ProviderBillingOverviewRow}; +pub(crate) use billing::{ProviderBillingConnection, ProviderBillingMetric};As per coding guidelines, “In
mod.rs, … avoid re-exporting private SQL row structs.”🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/types/providers/mod.rs` at line 16, Remove ProviderBillingOverviewRow from the pub(crate) re-export in the providers module, while continuing to re-export ProviderBillingConnection and ProviderBillingMetric. Keep ProviderBillingOverviewRow accessible only within the billing repository/row implementation.Source: Coding guidelines
src/types/providers/requests.rs (1)
5-11: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDocument the new public billing API contracts.
src/types/providers/requests.rs#L5-L11: document update and credential-preservation semantics.src/types/providers/responses.rs#L8-L110: document status, metric, monetary, and timestamp semantics.As per coding guidelines, “Document public APIs with
///doc comments.”🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/types/providers/requests.rs` around lines 5 - 11, The public billing API types need `///` documentation. In src/types/providers/requests.rs lines 5-11, document UpdateProviderBillingRequest and clarify update behavior, including how omitted credential values preserve existing credentials. In src/types/providers/responses.rs lines 8-110, document each public response type and field’s status, metric, monetary, and timestamp semantics as applicable.Source: Coding guidelines
frontend/pages/settings/providers.vue (1)
32-32: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winReplace fixed fallback/status colors with theme tokens.
Use
text-primary,bg-primary/10, andtext-primaryso these elements follow customized themes.As per coding guidelines: “Use customizable Tailwind tokens like
bg-primaryandtext-primaryinstead of fixed colors.”Also applies to: 224-225
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/pages/settings/providers.vue` at line 32, Replace the fixed `#6366f1` color in the BrainCircuit fallback/status styling and the corresponding elements around lines 224-225 with customizable Tailwind theme tokens: use text-primary for foreground/icon colors and bg-primary/10 for the background. Remove inline fixed-color styles while preserving the existing visual states and structure.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@frontend/pages/settings/providers.vue`:
- Around line 45-52: Rename the ShadCN components used throughout the settings
providers view to their auto-injected Shad-prefixed names, including Button,
Dialog, Input, Switch, Tabs, and related components. Update all template
references and remove any now-unnecessary imports, preserving the existing
props, events, and behavior.
- Around line 515-520: Update billingStatusKey so every UNAUTHORIZED status
returns settings.providers.billing.failed regardless of whether billing.upstream
exists; restrict settings.providers.billing.setup_required to NOT_CONFIGURED
only, while preserving the existing AVAILABLE and UNSUPPORTED mappings.
- Around line 636-650: Update addCustomProvider to reset the dialog’s active tab
to the default custom-provider tab before setting dialogOpen.value to true,
ensuring the dialog content and Save button render correctly regardless of the
previously selected tab.
- Around line 497-512: Update the provider-selection flow around loadBillingForm
so opening a provider immediately resets the billing form before starting the
request. In loadBillingForm, only apply the fetched billing values when the
currently selected provider still matches providerId; discard late responses for
previously selected providers to prevent stale data from overwriting the active
form.
In `@migrations/20260716000000_provider_billing.sql`:
- Around line 17-18: Update the provider_id foreign key in
provider_billing_snapshots to reference
provider_billing_connections(provider_id) with cascading deletes, replacing the
current providers(id) reference so snapshots cannot outlive their billing
connection.
In `@src/routes/admin/provider_billing.rs`:
- Line 62: Update the existing billing lookup in the admin provider flow to
propagate failures from Provider::find_billing_connection_for_admin as
InternalError instead of converting them to None via ok().flatten(). Preserve
the absent-connection behavior for successful queries that return no record,
while ensuring an existing OpenAI key with a database failure does not return
validation_failed.
In `@src/tests/encryption.rs`:
- Around line 29-33: Update the tampering test around decrypt_api_key to
Base64-decode encrypted before modifying it, flip a byte in the decoded
ciphertext/envelope rather than the trailing padding, then Base64-encode it
again. Assert that decryption returns the specific
EncryptionError::DecryptionFailed variant, preserving coverage of AES-GCM
authentication failure.
In `@src/types/providers/billing_repository.rs`:
- Around line 52-59: Update the upsert conflict handler in the billing
repository so changes to the credential or external scope invalidate cached
billing state transactionally. Detect whether the incoming credential,
external_scope_id, or external_scope_name differs from the existing
provider_billing_connections values, then reset or delete the associated
snapshot and status while preserving the current behavior for unchanged
configuration.
- Around line 170-177: Update the fallback match in the billing status logic to
include ProviderKind::Anthropic in the branch returning
ProviderBillingStatus::Unsupported, while preserving
ProviderBillingStatus::NotConfigured for other provider kinds.
In `@src/types/usage/repository.rs`:
- Around line 19-29: Replace the static query_as::<_, ProviderSpendRow>
monthly-spend query in src/types/usage/repository.rs:19-29 with the appropriate
checked SQLx query_as! macro, preserving its SQL, bindings, and returned fields.
In src/types/providers/billing_repository.rs:27-159, convert every static
runtime query_as and query call to the corresponding query_as! or query! macro,
ensuring all SQL shapes and bind types remain equivalent.
In `@src/utils/encryption.rs`:
- Around line 219-226: Update the AlreadyExists branch in the key creation match
to retry read_key_file(path) until the creator finishes writing, using a bounded
timeout and an appropriate short delay between attempts. Preserve immediate
success for a complete key and return the final read error when the timeout
expires.
In `@src/utils/provider_billing/mod.rs`:
- Around line 14-15: Document every newly exposed provider-billing API with
accurate /// comments: in src/utils/provider_billing/mod.rs (lines 14-15), cover
the public ProviderBillingError type, code, and refresh function; in
src/utils/provider_billing/openai.rs (line 51), document fetch_metric and its
errors; in src/utils/provider_billing/openrouter.rs (lines 35-40), document both
public fetch functions and their errors; and in
src/routes/admin/provider_billing.rs (line 18), document each public route
handler. Include # Errors and # Panics sections wherever applicable, and make no
other changes.
In `@src/utils/provider_billing/openai.rs`:
- Around line 38-44: Update the spend-alert pagination loop and AlertPage to use
the endpoint’s cursor contract: track the response’s after/last_id values
instead of page/next_page, and pass the cursor using the after and last_id
request parameters. Preserve has_more termination and ensure multi-page project
alerts continue until no further cursor is provided.
In `@src/utils/provider_billing/openrouter.rs`:
- Around line 16-22: Update the KeyData deserialization fields so limit_reset is
represented as its reset-type value rather than a timestamp, and select the
matching period-specific usage field (usage_daily, usage_weekly, or
usage_monthly) instead of always using usage_monthly. Preserve serde defaults
and ensure downstream billing logic consumes the selected period consistently.
---
Nitpick comments:
In `@frontend/pages/settings/providers.vue`:
- Line 32: Replace the fixed `#6366f1` color in the BrainCircuit fallback/status
styling and the corresponding elements around lines 224-225 with customizable
Tailwind theme tokens: use text-primary for foreground/icon colors and
bg-primary/10 for the background. Remove inline fixed-color styles while
preserving the existing visual states and structure.
In `@src/types/providers/mod.rs`:
- Line 16: Remove ProviderBillingOverviewRow from the pub(crate) re-export in
the providers module, while continuing to re-export ProviderBillingConnection
and ProviderBillingMetric. Keep ProviderBillingOverviewRow accessible only
within the billing repository/row implementation.
In `@src/types/providers/requests.rs`:
- Around line 5-11: The public billing API types need `///` documentation. In
src/types/providers/requests.rs lines 5-11, document
UpdateProviderBillingRequest and clarify update behavior, including how omitted
credential values preserve existing credentials. In
src/types/providers/responses.rs lines 8-110, document each public response type
and field’s status, metric, monetary, and timestamp semantics as applicable.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 07de8515-5cd2-4292-b1b1-79a91fcc0392
📒 Files selected for processing (28)
.env.example.intent/.gitignorefrontend/pages/settings/providers.vuefrontend/stores/providerBillingStore.tsfrontend/types/providers.tsmigrations/20260716000000_provider_billing.sqlsrc/ai.rssrc/jobs.rssrc/main.rssrc/routes/admin/mod.rssrc/routes/admin/provider_billing.rssrc/routes/admin/providers.rssrc/routes/mod.rssrc/tests/encryption.rssrc/tests/mod.rssrc/types/providers/billing.rssrc/types/providers/billing_repository.rssrc/types/providers/mod.rssrc/types/providers/requests.rssrc/types/providers/responses.rssrc/types/usage/repository.rssrc/utils/encryption.rssrc/utils/mod.rssrc/utils/provider_billing/mod.rssrc/utils/provider_billing/openai.rssrc/utils/provider_billing/openrouter.rssrc/utils/providers.rssrc/utils/tools/builtin/imagegen.rs
💤 Files with no reviewable changes (1)
- .intent/.gitignore
| <Button variant="outline" size="sm" class="gap-2" @click="openConfigDialog(item)"> | ||
| <Settings2 class="h-4 w-4" /> | ||
| <span v-if="!item.isConfigured">{{ store.getTranslation('settings.providers.configure') }}</span> | ||
| </Button> | ||
| <Button v-if="item.isConfigured" variant="outline" size="sm" class="gap-2" @click="syncProvider(item)"> | ||
| <RotateCw class="h-4 w-4" /> | ||
| </Button> | ||
| <Switch :modelValue="item.is_enabled || false" :disabled="!item.isConfigured" @update:modelValue="(val: boolean) => toggleProvider(item, val)" /> |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Use the auto-injected Shad* component names consistently.
Replace imported Button, Dialog, Input, Switch, Tabs, and related components with ShadButton, ShadDialog, ShadInput, ShadSwitch, ShadTabs, etc.
As per coding guidelines: “Use Shad*-prefixed auto-injected ShadCN components such as ShadButton and ShadSelect.”
Also applies to: 65-75, 95-247, 260-265
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/pages/settings/providers.vue` around lines 45 - 52, Rename the
ShadCN components used throughout the settings providers view to their
auto-injected Shad-prefixed names, including Button, Dialog, Input, Switch,
Tabs, and related components. Update all template references and remove any
now-unnecessary imports, preserving the existing props, events, and behavior.
Source: Coding guidelines
| dialogOpen.value = true; | ||
| if (item.isConfigured) loadBillingForm(item.id); | ||
| } | ||
|
|
||
| if (!isAlreadyAdded) { | ||
| result.push({ | ||
| kind: template.kind, | ||
| name: template.name, | ||
| description: template.description, | ||
| template, | ||
| isConfigured: false, | ||
| is_enabled: false, | ||
| }); | ||
| } | ||
| async function loadBillingForm(providerId: string) { | ||
| try { | ||
| const billing = await billingStore.fetchProviderBilling(providerId); | ||
| billingForm.isEnabled = billing.is_enabled; | ||
| billingForm.credential = ''; | ||
| billingForm.scopeId = billing.external_scope_id ?? ''; | ||
| billingForm.scopeName = billing.external_scope_name ?? ''; | ||
| billingForm.hasCredential = billing.has_billing_credential; | ||
| billingForm.hasConnection = billing.is_enabled || billing.has_billing_credential || billing.external_scope_id !== null; | ||
| } catch (error) { | ||
| console.error('Failed to load billing configuration:', error); | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Prevent stale billing requests from overwriting another provider’s form.
Opening provider B while provider A’s request is pending allows A’s late response to populate B’s form; saving then writes A’s scope/enabled state to B. Reset immediately and discard responses unless the selected provider still matches providerId.
Proposed fix
dialogOpen.value = true;
- if (item.isConfigured) loadBillingForm(item.id);
+ Object.assign(billingForm, {
+ isEnabled: false,
+ credential: '',
+ scopeId: '',
+ scopeName: '',
+ hasCredential: false,
+ hasConnection: false,
+ });
+ if (item.isConfigured) void loadBillingForm(item.id);
}
async function loadBillingForm(providerId: string) {
try {
const billing = await billingStore.fetchProviderBilling(providerId);
+ if (configForm.existingProvider?.id !== providerId) return;
billingForm.isEnabled = billing.is_enabled;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| dialogOpen.value = true; | |
| if (item.isConfigured) loadBillingForm(item.id); | |
| } | |
| if (!isAlreadyAdded) { | |
| result.push({ | |
| kind: template.kind, | |
| name: template.name, | |
| description: template.description, | |
| template, | |
| isConfigured: false, | |
| is_enabled: false, | |
| }); | |
| } | |
| async function loadBillingForm(providerId: string) { | |
| try { | |
| const billing = await billingStore.fetchProviderBilling(providerId); | |
| billingForm.isEnabled = billing.is_enabled; | |
| billingForm.credential = ''; | |
| billingForm.scopeId = billing.external_scope_id ?? ''; | |
| billingForm.scopeName = billing.external_scope_name ?? ''; | |
| billingForm.hasCredential = billing.has_billing_credential; | |
| billingForm.hasConnection = billing.is_enabled || billing.has_billing_credential || billing.external_scope_id !== null; | |
| } catch (error) { | |
| console.error('Failed to load billing configuration:', error); | |
| } | |
| dialogOpen.value = true; | |
| Object.assign(billingForm, { | |
| isEnabled: false, | |
| credential: '', | |
| scopeId: '', | |
| scopeName: '', | |
| hasCredential: false, | |
| hasConnection: false, | |
| }); | |
| if (item.isConfigured) void loadBillingForm(item.id); | |
| } | |
| async function loadBillingForm(providerId: string) { | |
| try { | |
| const billing = await billingStore.fetchProviderBilling(providerId); | |
| if (configForm.existingProvider?.id !== providerId) return; | |
| billingForm.isEnabled = billing.is_enabled; | |
| billingForm.credential = ''; | |
| billingForm.scopeId = billing.external_scope_id ?? ''; | |
| billingForm.scopeName = billing.external_scope_name ?? ''; | |
| billingForm.hasCredential = billing.has_billing_credential; | |
| billingForm.hasConnection = billing.is_enabled || billing.has_billing_credential || billing.external_scope_id !== null; | |
| } catch (error) { | |
| console.error('Failed to load billing configuration:', error); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/pages/settings/providers.vue` around lines 497 - 512, Update the
provider-selection flow around loadBillingForm so opening a provider immediately
resets the billing form before starting the request. In loadBillingForm, only
apply the fetched billing values when the currently selected provider still
matches providerId; discard late responses for previously selected providers to
prevent stale data from overwriting the active form.
| function billingStatusKey(billing: ProviderBillingOverview) { | ||
| if ((billing.status === 'UPSTREAM_ERROR' || billing.status === 'UNAUTHORIZED') && billing.upstream) return 'settings.providers.billing.failed'; | ||
| if (billing.status === 'AVAILABLE' && billing.upstream) return 'settings.providers.billing.provider_reported'; | ||
| if (billing.status === 'UNSUPPORTED') return 'settings.providers.billing.unsupported'; | ||
| if (billing.status === 'NOT_CONFIGURED' || billing.status === 'UNAUTHORIZED') return 'settings.providers.billing.setup_required'; | ||
| return 'settings.providers.billing.failed'; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Do not classify first-refresh authorization failures as missing setup.
When status === 'UNAUTHORIZED' and no prior snapshot exists, Line 519 returns setup_required. Classify UNAUTHORIZED as failed regardless of upstream; reserve setup-required for NOT_CONFIGURED.
Proposed fix
function billingStatusKey(billing: ProviderBillingOverview) {
- if ((billing.status === 'UPSTREAM_ERROR' || billing.status === 'UNAUTHORIZED') && billing.upstream) return 'settings.providers.billing.failed';
+ if (billing.status === 'UPSTREAM_ERROR' || billing.status === 'UNAUTHORIZED') return 'settings.providers.billing.failed';
if (billing.status === 'AVAILABLE' && billing.upstream) return 'settings.providers.billing.provider_reported';
if (billing.status === 'UNSUPPORTED') return 'settings.providers.billing.unsupported';
- if (billing.status === 'NOT_CONFIGURED' || billing.status === 'UNAUTHORIZED') return 'settings.providers.billing.setup_required';
+ if (billing.status === 'NOT_CONFIGURED') return 'settings.providers.billing.setup_required';
return 'settings.providers.billing.failed';
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function billingStatusKey(billing: ProviderBillingOverview) { | |
| if ((billing.status === 'UPSTREAM_ERROR' || billing.status === 'UNAUTHORIZED') && billing.upstream) return 'settings.providers.billing.failed'; | |
| if (billing.status === 'AVAILABLE' && billing.upstream) return 'settings.providers.billing.provider_reported'; | |
| if (billing.status === 'UNSUPPORTED') return 'settings.providers.billing.unsupported'; | |
| if (billing.status === 'NOT_CONFIGURED' || billing.status === 'UNAUTHORIZED') return 'settings.providers.billing.setup_required'; | |
| return 'settings.providers.billing.failed'; | |
| function billingStatusKey(billing: ProviderBillingOverview) { | |
| if (billing.status === 'UPSTREAM_ERROR' || billing.status === 'UNAUTHORIZED') return 'settings.providers.billing.failed'; | |
| if (billing.status === 'AVAILABLE' && billing.upstream) return 'settings.providers.billing.provider_reported'; | |
| if (billing.status === 'UNSUPPORTED') return 'settings.providers.billing.unsupported'; | |
| if (billing.status === 'NOT_CONFIGURED') return 'settings.providers.billing.setup_required'; | |
| return 'settings.providers.billing.failed'; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/pages/settings/providers.vue` around lines 515 - 520, Update
billingStatusKey so every UNAUTHORIZED status returns
settings.providers.billing.failed regardless of whether billing.upstream exists;
restrict settings.providers.billing.setup_required to NOT_CONFIGURED only, while
preserving the existing AVAILABLE and UNSUPPORTED mappings.
| function addCustomProvider() { | ||
| selectedProvider.value = { | ||
| kind: 'OPENAI_COMPAT', | ||
| name: '', | ||
| description: '', | ||
| icon: null, | ||
| brandColor: '#6366f1', | ||
| isPreConfigured: false, | ||
| }; | ||
| configForm.name = ''; | ||
| configForm.apiKey = ''; | ||
| configForm.baseUrl = ''; | ||
| configForm.isEnabled = true; | ||
| configForm.existingProvider = null; | ||
| dialogOpen.value = true; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Reset the active tab before opening the custom-provider dialog.
If the previous dialog was on billing or catalog, the custom dialog opens without matching tab content; on catalog, the Save button also disappears.
Proposed fix
function addCustomProvider() {
+ activeProviderTab.value = 'settings';
+ catalogLoaded.value = false;
+ catalogModels.value = [];
+ catalogSearch.value = '';
selectedProvider.value = {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function addCustomProvider() { | |
| selectedProvider.value = { | |
| kind: 'OPENAI_COMPAT', | |
| name: '', | |
| description: '', | |
| icon: null, | |
| brandColor: '#6366f1', | |
| isPreConfigured: false, | |
| }; | |
| configForm.name = ''; | |
| configForm.apiKey = ''; | |
| configForm.baseUrl = ''; | |
| configForm.isEnabled = true; | |
| configForm.existingProvider = null; | |
| dialogOpen.value = true; | |
| function addCustomProvider() { | |
| activeProviderTab.value = 'settings'; | |
| catalogLoaded.value = false; | |
| catalogModels.value = []; | |
| catalogSearch.value = ''; | |
| selectedProvider.value = { | |
| kind: 'OPENAI_COMPAT', | |
| name: '', | |
| description: '', | |
| icon: null, | |
| brandColor: '`#6366f1`', | |
| isPreConfigured: false, | |
| }; | |
| configForm.name = ''; | |
| configForm.apiKey = ''; | |
| configForm.baseUrl = ''; | |
| configForm.isEnabled = true; | |
| configForm.existingProvider = null; | |
| dialogOpen.value = true; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/pages/settings/providers.vue` around lines 636 - 650, Update
addCustomProvider to reset the dialog’s active tab to the default
custom-provider tab before setting dialogOpen.value to true, ensuring the dialog
content and Save button render correctly regardless of the previously selected
tab.
| CREATE TABLE provider_billing_snapshots ( | ||
| provider_id UUID PRIMARY KEY REFERENCES providers(id) ON DELETE CASCADE, |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Tie snapshots to billing connections to prevent orphaned cached data.
A refresh that already loaded a connection can insert a snapshot after an admin concurrently deletes that connection, because this FK only requires the provider to exist. Reference provider_billing_connections(provider_id) so connection deletion atomically removes and prevents snapshots.
Proposed fix
CREATE TABLE provider_billing_snapshots (
- provider_id UUID PRIMARY KEY REFERENCES providers(id) ON DELETE CASCADE,
+ provider_id UUID PRIMARY KEY REFERENCES provider_billing_connections(provider_id) ON DELETE CASCADE,📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| CREATE TABLE provider_billing_snapshots ( | |
| provider_id UUID PRIMARY KEY REFERENCES providers(id) ON DELETE CASCADE, | |
| CREATE TABLE provider_billing_snapshots ( | |
| provider_id UUID PRIMARY KEY REFERENCES provider_billing_connections(provider_id) ON DELETE CASCADE, |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@migrations/20260716000000_provider_billing.sql` around lines 17 - 18, Update
the provider_id foreign key in provider_billing_snapshots to reference
provider_billing_connections(provider_id) with cascading deletes, replacing the
current providers(id) reference so snapshots cannot outlive their billing
connection.
| let rows = sqlx::query_as::<_, ProviderSpendRow>( | ||
| r#"SELECT provider_id AS provider_id, COALESCE(SUM(cost_total), 0)::numeric AS spent_amount | ||
| FROM usage_events | ||
| WHERE provider_id = ANY($1) | ||
| AND created_at >= date_trunc('month', NOW() AT TIME ZONE 'UTC') AT TIME ZONE 'UTC' | ||
| AND created_at < (date_trunc('month', NOW() AT TIME ZONE 'UTC') + INTERVAL '1 month') AT TIME ZONE 'UTC' | ||
| GROUP BY provider_id"#, | ||
| ) | ||
| .bind(provider_ids) | ||
| .fetch_all(pool) | ||
| .await?; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
set -euo pipefail
echo "== files =="
git ls-files 'src/types/usage/repository.rs' 'src/types/providers/billing_repository.rs' '.sqlx/*' | sed 's/^/ - /'
echo
echo "== usage repository =="
cat -n src/types/usage/repository.rs
echo
echo "== billing repository (selected ranges) =="
sed -n '1,240p' src/types/providers/billing_repository.rs | cat -n
echo
echo "== sqlx metadata presence =="
find .sqlx -maxdepth 1 -type f 2>/dev/null | sed 's/^/ - /' | head -n 80Repository: NxtCore/OxideChat
Length of output: 20630
🏁 Script executed:
set -euo pipefail
echo "== code search for existing checked macros in billing path =="
rg -n "query_as!|query!" src/types/providers src/types/usage || true
echo
echo "== list sqlx metadata entries mentioning billing/provider usage =="
rg -n "provider_billing|usage_events|provider_id|spent_amount" .sqlx || trueRepository: NxtCore/OxideChat
Length of output: 331
🏁 Script executed:
set -euo pipefail
echo "== locate related type definitions =="
rg -n "struct ProviderSpendRow|struct ProviderBillingConnection|struct ProviderBillingOverviewRow|enum ProviderBillingStatus|struct ProviderBillingMetric|struct UpdateProviderBillingRequest" src/types -g '*.rs'
echo
echo "== read type definitions around matches =="
for f in \
$(rg -l "struct ProviderSpendRow|struct ProviderBillingConnection|struct ProviderBillingOverviewRow|enum ProviderBillingStatus|struct ProviderBillingMetric|struct UpdateProviderBillingRequest" src/types -g '*.rs' | sort -u); do
echo "--- $f ---"
sed -n '1,260p' "$f" | cat -n
done
echo
echo "== offline sqlx metadata files =="
find .sqlx -type f | sed 's/^/ - /' | sort | head -n 200Repository: NxtCore/OxideChat
Length of output: 19076
🏁 Script executed:
set -euo pipefail
# Show the definitions for the row/DTO types used by the reviewed queries.
for sym in ProviderSpendRow ProviderBillingConnection ProviderBillingOverviewRow ProviderBillingStatus ProviderBillingMetric UpdateProviderBillingRequest; do
echo "== $sym =="
rg -n "$sym" src/types -g '*.rs' || true
echo
done
# Read only the files that define the matched types, if any.
files=$(rg -l "ProviderSpendRow|ProviderBillingConnection|ProviderBillingOverviewRow|ProviderBillingStatus|ProviderBillingMetric|UpdateProviderBillingRequest" src/types -g '*.rs' | sort -u)
for f in $files; do
echo "--- $f ---"
wc -l "$f"
sed -n '1,220p' "$f" | cat -n
done
echo
echo "== sqlx offline metadata candidates =="
find .sqlx -type f 2>/dev/null | sort | sed -n '1,120p'Repository: NxtCore/OxideChat
Length of output: 37945
Switch these static SQL statements to checked SQLx macros
src/types/usage/repository.rs:19-29still usesquery_as::<_, ProviderSpendRow>for a static monthly-spend read.src/types/providers/billing_repository.rs:27-159still uses runtimequery_as/queryfor static reads and writes.
Convert them to query_as!/query! so the SQL shape and bind types are checked at compile time.
📍 Affects 2 files
src/types/usage/repository.rs#L19-L29(this comment)src/types/providers/billing_repository.rs#L27-L159
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/types/usage/repository.rs` around lines 19 - 29, Replace the static
query_as::<_, ProviderSpendRow> monthly-spend query in
src/types/usage/repository.rs:19-29 with the appropriate checked SQLx query_as!
macro, preserving its SQL, bindings, and returned fields. In
src/types/providers/billing_repository.rs:27-159, convert every static runtime
query_as and query call to the corresponding query_as! or query! macro, ensuring
all SQL shapes and bind types remain equivalent.
Source: Coding guidelines
| match options.open(path) { | ||
| Ok(mut file) => { | ||
| file.write_all(encoded.as_bytes())?; | ||
| file.write_all(b"\n")?; | ||
| file.sync_all()?; | ||
| Ok((key, KeySource::Generated)) | ||
| } | ||
| Err(error) if error.kind() == ErrorKind::AlreadyExists => read_key_file(path).map(|key| (key, KeySource::File)), |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Handle concurrent key-file creation without reading a partial file.
create_new publishes the empty file before Lines 221-223 finish writing. A concurrent instance can hit AlreadyExists, immediately read the incomplete file, report InvalidKeyFile, and exit. Retry the read until the creator finishes, with a bounded timeout.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/utils/encryption.rs` around lines 219 - 226, Update the AlreadyExists
branch in the key creation match to retry read_key_file(path) until the creator
finishes writing, using a bounded timeout and an appropriate short delay between
attempts. Preserve immediate success for a complete key and return the final
read error when the timeout expires.
| #[derive(Debug, Error, Clone, Copy)] | ||
| pub enum ProviderBillingError { |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Document all newly exposed provider-billing APIs.
src/utils/provider_billing/mod.rs#L14-L15: document the public error type,code, and refresh function.src/utils/provider_billing/openai.rs#L51-L51: documentfetch_metricand its errors.src/utils/provider_billing/openrouter.rs#L35-L40: document both public fetch functions and their errors.src/routes/admin/provider_billing.rs#L18-L18: document each public route handler.
As per coding guidelines: “Document public APIs with /// doc comments” and “Include # Errors and # Panics sections in documentation where applicable.”
📍 Affects 4 files
src/utils/provider_billing/mod.rs#L14-L15(this comment)src/utils/provider_billing/openai.rs#L51-L51src/utils/provider_billing/openrouter.rs#L35-L40src/routes/admin/provider_billing.rs#L18-L18
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/utils/provider_billing/mod.rs` around lines 14 - 15, Document every newly
exposed provider-billing API with accurate /// comments: in
src/utils/provider_billing/mod.rs (lines 14-15), cover the public
ProviderBillingError type, code, and refresh function; in
src/utils/provider_billing/openai.rs (line 51), document fetch_metric and its
errors; in src/utils/provider_billing/openrouter.rs (lines 35-40), document both
public fetch functions and their errors; and in
src/routes/admin/provider_billing.rs (line 18), document each public route
handler. Include # Errors and # Panics sections wherever applicable, and make no
other changes.
Source: Coding guidelines
| struct AlertPage { | ||
| #[serde(default)] | ||
| data: Vec<SpendAlert>, | ||
| #[serde(default)] | ||
| has_more: bool, | ||
| next_page: Option<String>, | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
curl -fsSL https://raw.githubusercontent.com/openai/openai-python/main/src/openai/resources/admin/organization/projects/spend_alerts.py |
rg -n 'after|SyncConversationCursorPage'
curl -fsSL https://raw.githubusercontent.com/openai/openai-python/main/src/openai/pagination.py |
rg -n 'SyncConversationCursorPage|last_id|after'Repository: NxtCore/OxideChat
Length of output: 1268
🏁 Script executed:
#!/bin/bash
set -euo pipefail
file='src/utils/provider_billing/openai.rs'
wc -l "$file"
printf '\n--- lines 1-220 ---\n'
sed -n '1,220p' "$file" | cat -nRepository: NxtCore/OxideChat
Length of output: 5850
Use the spend-alert cursor contract here. spend_alerts paginates with after and last_id, not page/next_page, so this will fail with InvalidResponse once a project spans multiple alert pages. Update both this loop and the AlertPage fields to match the endpoint.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/utils/provider_billing/openai.rs` around lines 38 - 44, Update the
spend-alert pagination loop and AlertPage to use the endpoint’s cursor contract:
track the response’s after/last_id values instead of page/next_page, and pass
the cursor using the after and last_id request parameters. Preserve has_more
termination and ensure multi-page project alerts continue until no further
cursor is provided.
| struct KeyData { | ||
| limit: Option<Decimal>, | ||
| limit_remaining: Option<Decimal>, | ||
| limit_reset: Option<String>, | ||
| #[serde(default)] | ||
| usage_monthly: Decimal, | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
curl -fsSL https://openrouter.ai/docs/api/reference/limits.md |
rg -n 'limit_reset|usage_daily|usage_weekly|usage_monthly'Repository: NxtCore/OxideChat
Length of output: 952
🏁 Script executed:
#!/bin/bash
set -euo pipefail
tmp="$(mktemp)"
curl -fsSL https://openrouter.ai/docs/api/reference/limits.md >"$tmp"
sed -n '280,330p' "$tmp"Repository: NxtCore/OxideChat
Length of output: 2767
limit_reset is a reset type, not a timestamp. Use the period-specific usage field (usage_daily, usage_weekly, or usage_monthly) instead of always reading usage_monthly.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/utils/provider_billing/openrouter.rs` around lines 16 - 22, Update the
KeyData deserialization fields so limit_reset is represented as its reset-type
value rather than a timestamp, and select the matching period-specific usage
field (usage_daily, usage_weekly, or usage_monthly) instead of always using
usage_monthly. Preserve serde defaults and ensure downstream billing logic
consumes the selected period consistently.
…ent, teams, tooling, and default models migration files.
…d, backend, and database to improve consistency and data integrity.
Greptile Summary
This PR adds upstream provider billing and cached budget views. The main changes are:
Confidence Score: 4/5
Provider status handling needs fixes before merging.
src/types/providers/billing_repository.rs; frontend/pages/settings/providers.vue
Important Files Changed
Sequence Diagram
%%{init: {'theme': 'neutral'}}%% sequenceDiagram participant UI as Providers UI participant API as Admin Billing API participant DB as Billing Cache participant Upstream as OpenAI/OpenRouter participant Job as Refresh Job UI->>API: Load billing overviews API->>DB: Read snapshots and local spend DB-->>API: Cached billing data API-->>UI: Render provider cards UI->>API: Save encrypted configuration API->>DB: Upsert billing connection UI->>API: Request manual refresh API->>Upstream: Fetch billing metrics Upstream-->>API: Costs, thresholds, or credits API->>DB: Save snapshot or failure status API-->>UI: Return updated overview Job->>DB: List enabled connections loop Four concurrent refreshes Job->>Upstream: Fetch billing metrics Job->>DB: Save snapshot or failure status end%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%% sequenceDiagram participant UI as Providers UI participant API as Admin Billing API participant DB as Billing Cache participant Upstream as OpenAI/OpenRouter participant Job as Refresh Job UI->>API: Load billing overviews API->>DB: Read snapshots and local spend DB-->>API: Cached billing data API-->>UI: Render provider cards UI->>API: Save encrypted configuration API->>DB: Upsert billing connection UI->>API: Request manual refresh API->>Upstream: Fetch billing metrics Upstream-->>API: Costs, thresholds, or credits API->>DB: Save snapshot or failure status API-->>UI: Return updated overview Job->>DB: List enabled connections loop Four concurrent refreshes Job->>Upstream: Fetch billing metrics Job->>DB: Save snapshot or failure status endReviews (1): Last reviewed commit: "Implement provider billing synchronizati..." | Re-trigger Greptile
Context used (4)
Summary by CodeRabbit
New Features
Bug Fixes